/*
 * Copyright 2012-2013 EZC Group S.A. RCS Luxembourg B140949
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.yobidrive.y3.v1.client.http;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.SimpleTimeZone;

import javax.crypto.SecretKey;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.yobidrive.y3.client.Y3Client;
import com.yobidrive.y3.exceptions.Y3ErrorCode;
import com.yobidrive.y3.exceptions.Y3Exception;
import com.yobidrive.y3.http.HttpClientFactory;
import com.yobidrive.y3.http.HttpMethod;
import com.yobidrive.y3.utils.CryptoUtils;
import com.yobidrive.y3.utils.XMLUtils;
import com.yobidrive.y3.v1.Y3;
import com.yobidrive.y3.v1.client.http.handlers.CreateFlowEventResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.CreateFlowResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.DeleteNodeResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.EmptyReturnResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.FinalizePairingResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetAttachedKeysListResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetBrandingResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetDriveDetailsResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetDriveListResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetEditionResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetExternalAccountPairingResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetFlowEventResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetFlowResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetFlowUpdateResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetFlowsConfigurationResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetFlowsListResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetItemLinkDetailsResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetLeafContentMetadataAndStreamResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetNodeContentResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetNodeDetailsResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetPairingStatusResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetUserConfigurationResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.GetUserListResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.ManageMemberResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.RequestChallengeResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.RequestEncodedUserLockResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.RequestPairingResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.RequestTokenResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.RequestUnlockResponseHandler;
import com.yobidrive.y3.v1.client.http.handlers.StringReturnResponseHandler;
import com.yobidrive.y3.v1.model.CreateFlowEventResult;
import com.yobidrive.y3.v1.model.CreateFlowResult;
import com.yobidrive.y3.v1.model.DeleteNodeResult;
import com.yobidrive.y3.v1.model.FinalizePairingResult;
import com.yobidrive.y3.v1.model.GetAttachedKeysListResult;
import com.yobidrive.y3.v1.model.GetBrandingResult;
import com.yobidrive.y3.v1.model.GetDriveDetailsResult;
import com.yobidrive.y3.v1.model.GetDriveListResult;
import com.yobidrive.y3.v1.model.GetEditionResult;
import com.yobidrive.y3.v1.model.GetExternalAccountPairingResult;
import com.yobidrive.y3.v1.model.GetFlowEventResult;
import com.yobidrive.y3.v1.model.GetFlowResult;
import com.yobidrive.y3.v1.model.GetFlowUpdateResult;
import com.yobidrive.y3.v1.model.GetFlowsConfigurationResult;
import com.yobidrive.y3.v1.model.GetFlowsListResult;
import com.yobidrive.y3.v1.model.GetItemLinkDetailsResult;
import com.yobidrive.y3.v1.model.GetNodeContentResult;
import com.yobidrive.y3.v1.model.GetNodeDetailsResult;
import com.yobidrive.y3.v1.model.GetPairingStatusResult;
import com.yobidrive.y3.v1.model.GetUserConfigurationResult;
import com.yobidrive.y3.v1.model.GetUserListResult;
import com.yobidrive.y3.v1.model.KeyValue;
import com.yobidrive.y3.v1.model.LeafContentMetadata;
import com.yobidrive.y3.v1.model.ManageMemberResult;
import com.yobidrive.y3.v1.model.RequestChallengeResult;
import com.yobidrive.y3.v1.model.RequestEncodedUserLockResult;
import com.yobidrive.y3.v1.model.RequestPairingResult;
import com.yobidrive.y3.v1.model.RequestTokenResult;
import com.yobidrive.y3.v1.model.RequestUnlockResult;

/**
 * Implementation of the Y3 Client that uses HTTP calls to the
 * YobiDrive endpoint to do is work.
 * 
 * @author david
 *
 */
public final class Y3HttpClient extends Y3Client implements Y3{
	static private final Logger logger = Logger.getLogger(Y3HttpClient.class);
	static private final String ENDPOINT = "y3endpoint";
	static private final String Y3APIKEY = "y3apikey";
	static private final String Y3APISECRET = "y3apisecret";
	
	/** End point to the service, red from the properties at initialization time **/
	private String endPoint = null;
	private String scheme   = null;
	private String host	    = null;
	private String basePath = null;
	private String apiKey	= null;
	private String apiSecret= null;
	//Date format for api signatures
	private final SimpleDateFormat rfc822DateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
	
	/** Token used by the client **/
	private String userName		= null;
	private String userPass		= null; //Keep the SHA version only
	private String token 		= null; //Access token to Y3 service, validates user identity
	private String safeSecret	= null; //Safe secret in case of eSafe
	private String keySerial	= null; //Key serial
	
	/** Additional parameters **/
	private String originatingAddr = null;
	private String userUUID = null; //Unique client identifier for direct accesses
	
	@Override
	/**
	 * Recover endpoint from properies file.
	 */
	public final void initialize(Properties properties) throws Y3Exception {
		if ( properties == null ) throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Properties are null");
		
		endPoint = properties.getProperty(ENDPOINT);
		if ( endPoint == null || "".equals(endPoint))
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Endpoint not found in configuration file");
		
		if ( "/".equals(endPoint.substring(endPoint.length()-1))) 
			endPoint = endPoint.substring(0,endPoint.length()-1);
		
		//Parse endpoints parts: scheme, host, basepath
		// https://hostY3/basePath
		// 012345678
		int schemeDelimiterIndx = endPoint.indexOf(":");
		scheme = endPoint.substring(0, schemeDelimiterIndx);
		int hostDelimiterIndx = endPoint.indexOf("/",schemeDelimiterIndx+3);
		if ( hostDelimiterIndx < 0 ) {
			host = endPoint.substring(schemeDelimiterIndx+3,hostDelimiterIndx);
			basePath = "/";
		} else {
			host = endPoint.substring(schemeDelimiterIndx+3,hostDelimiterIndx);
			basePath = endPoint.substring(hostDelimiterIndx);
		}
		
		//Check for api keys
		apiKey = properties.getProperty(Y3APIKEY);
		apiSecret = properties.getProperty(Y3APISECRET);
		rfc822DateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));
		
	}

	
	//----------------  AUTHENTICATION ------------------//
	
	
	@Override
	/**
	 * Handle two-phases login process:
	 * 	- Request a challenge
	 * 	- Respond to the challenge to recover the token
	 */
	public String login(String userName, String password) throws Y3Exception {
		if ( userName == null || "".equals(userName) || password == null || "".equals(password)) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Missing userName and/or password");
		}
		
		//Keep name 
		this.userName = userName;
		
		//Recover a challenge
		RequestChallengeResult challengeResponse = requestChallenge(); 
		//Compute the response to the challenge -> need to recover userName and nonce
		if ( challengeResponse == null || challengeResponse.challenge == null ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Invalid challenge");
		}
		
		String response = computeChallengeResponse(challengeResponse, userName, password);
		RequestTokenResult tokenResponse = requestToken(userName,challengeResponse.challenge, response, false);
		token = tokenResponse.token;
		return token;
	}
	
	/**
	 * Call the requestChallenge REST method server-side
	 * @return
	 * @throws Y3Exception
	 */
	private RequestChallengeResult requestChallenge() throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("challenge", "1"));
			
		RequestChallengeResult response = sendHttpRequest(HttpMethod.POST, "/",qparams, new RequestChallengeResponseHandler(), null);
		return response;
	}
	
	/**
	 * Call the requestToken REST method server-side
	 * @param userName
	 * @param challenge
	 * @param challengeResponse
	 * @return
	 * @throws Y3Exception
	 */
	private RequestTokenResult requestToken(String userName, Node challenge, String challengeResponse, boolean isRefresh) throws Y3Exception {
		//Prepare content to be sent
		Document doc = null;
		String responseContent = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("requesttoken");
			rootNode.appendChild(doc.importNode(challenge,true));
			Element challengeResponseElt = doc.createElement("challengeresponse");
			challengeResponseElt.setTextContent(challengeResponse);
			rootNode.appendChild(challengeResponseElt);
			responseContent = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		if ( isRefresh)
			qparams.add(new BasicNameValuePair("refresh", "1"));
		else
			qparams.add(new BasicNameValuePair("token", "1"));
		
		RequestTokenResult response	= sendHttpRequest(HttpMethod.POST, "/", qparams, new RequestTokenResponseHandler(), responseContent);
		return response;
	}
	
	/**
	 * 
	 * @param userName
	 * @param password
	 * @return
	 */
	private String computeChallengeResponse(RequestChallengeResult challengeResponse, String userName, String password) throws Y3Exception {
		try {
			password  = CryptoUtils.hash(password);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error hashing password",ex);
		}
		
		String username = null;
		String nonce	= null;
		
		NodeList usernameList = challengeResponse.challenge.getElementsByTagName("username");
		if ( usernameList != null && usernameList.getLength() > 0 ) {
			username = usernameList.item(0).getTextContent();
		}
		NodeList nonceList = challengeResponse.challenge.getElementsByTagName("nonce");
		if ( nonceList != null && nonceList.getLength() > 0 ) {
			nonce = nonceList.item(0).getTextContent();
		}
		
		String toHMAC = username+nonce;
		String response;
		try {
			response = CryptoUtils.getHMAC( password , toHMAC);
		} catch (Exception ex ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error computing response to challenge",ex);
		}
		
		return response;
	}
	
	@Override
	/**
	 * Locally update the token and userName fields so that they are used in next calls 
	 */
	public void attachToken(String userName, String token) throws Y3Exception {
		this.userName = userName;
		this.token = token;
	}
	
	
	@Override
	public String refreshTokenBasic() throws Y3Exception{
		//Call the refresh service without any content
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("refresh", "1"));
		RequestTokenResult response	= sendHttpRequest(HttpMethod.POST, "/", qparams, new RequestTokenResponseHandler(),null);
		return response.token;
	}
	
	@Override
	public String refreshToken(String userName, String password)
			throws Y3Exception {
		return refreshToken(userName, password, this.token);
	}
	
	/**
	 * Refresh is very similar to the login phase only difference is that we already know our token id
	 * and just want to extend its validity period.
	 */
	public String refreshToken(String userName, String password, String token)
	throws Y3Exception {
		if ( token == null || "".equals(token) || password == null || "".equals(password)  || userName == null || "".equals(userName) ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER, "Missing parameters for refreshToken");
		}
		
		//Recover a challenge
		RequestChallengeResult challengeResponse = requestChallenge(); 
		//Compute the response to the challenge -> need to recover userName and nonce
		if ( challengeResponse == null || challengeResponse.challenge == null ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Invalid challenge");
		}
		
		String response = computeChallengeResponse(challengeResponse, userName, password);
		
		RequestTokenResult tokenResponse = requestToken(userName,challengeResponse.challenge, response, true);
		token = tokenResponse.token;
		return token;		
	}

	@Override
	/**
	 * Handle two-phases safe opening process:
	 * 	- Request a challenge
	 * 	- Respond to the challenge using our private key
	 */
	public String openSafe(String driveId, String secretBase64)
			throws Y3Exception {
		
		if ( driveId == null || "".equals(driveId)) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Missing driveId");
		}
		
		//Recover the secret key based on the secret
		SecretKey secretKey = CryptoUtils.getForStringSecretKey(secretBase64);
		//Compute key serial
		String keySerial = CryptoUtils.hash(secretKey.getEncoded());
		
		this.keySerial = keySerial;
		
		//Encapsulate calls to the getsafeopeningchallenge and the opensafe methods
		RequestEncodedUserLockResult challengeResult = requestEncodedUserLock(driveId);
		if ( challengeResult == null || challengeResult.encodedUserLock == null ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Invalid challenge");	
		}
		
		if ( "".equals(challengeResult.encodedUserLock) ) {
			//Empty challenge -> this is not an esafe thus nothing to open
			return "";
		}
		
		//Decrypt the secret using user private key
		String response = CryptoUtils.decryptSecretFromB64AndIVWithAESKey(challengeResult.encodedUserLock, secretKey);
		if ( response == null) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to decrypt challenge using private key");
		}
		
		//Send the response to the server to recover our safe secret
		RequestUnlockResult safeUnlockResult = requestUnlock(driveId, response);
		if ( safeUnlockResult == null || safeUnlockResult.userLockSecret == null || "".equals(safeUnlockResult.userLockSecret) ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INVALID_PARAMETER,"Invalid safe secret");	
		}
		
		attachSecret(safeUnlockResult.userLockSecret);
		return safeUnlockResult.userLockSecret;
	}

	/**
	 * Call the requestSafeOpening REST method server-side
	 * @return
	 * @throws Y3Exception
	 */
	private RequestEncodedUserLockResult requestEncodedUserLock(String driveId) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("encodeduserlock", driveId));
		RequestEncodedUserLockResult response = sendHttpRequest(HttpMethod.POST,"/", qparams, new RequestEncodedUserLockResponseHandler(), null);
		return response;
	}
	
	/**
	 * Call the openSafe REST method server-side
	 * @param userName
	 * @param encodedUserLock
	 * @param challengeResponse
	 * @return
	 * @throws Y3Exception
	 */
	private RequestUnlockResult requestUnlock(String driveId, String challengeResponse) throws Y3Exception {
		//Prepare content to be sent
		Document doc = null;
		String responseContent = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("requestsafesecret");
			Element driveElt = doc.createElement("drive");
			driveElt.setTextContent(driveId);
			rootNode.appendChild(driveElt);
			Element challengeResponseElt = doc.createElement("challengeresponse");
			challengeResponseElt.setTextContent(challengeResponse);
			rootNode.appendChild(challengeResponseElt);
			responseContent = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
	
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("unlock", "1"));
		RequestUnlockResult response	= sendHttpRequest(HttpMethod.POST,"/", qparams, new RequestUnlockResponseHandler(), responseContent);
		return response;
	}
	
	@Override
	public void attachSecret(String secret) throws Y3Exception {
		this.safeSecret = secret;
	}
	
	@Override
	public GetUserConfigurationResult getUserConfiguration(boolean withUsage) throws Y3Exception {
	    return getUserConfiguration(withUsage, false) ;
	}
	
	// @Override
	public GetUserConfigurationResult getUserConfiguration(boolean withUsage, boolean renewS3Key) throws Y3Exception {
		assertConnected();
		List<NameValuePair> qparams = null;
		if ( withUsage ) {
			qparams = new ArrayList<NameValuePair>();
			qparams.add(new BasicNameValuePair("usagedetails", "1"));
			// qparams.add(new BasicNameValuePair("renews3key", "1")) ;
		}
		return sendHttpRequest(HttpMethod.POST,"/account/configuration", qparams, new GetUserConfigurationResponseHandler(), null);
	}
	
	//----------------  PAIRING MANAGEMENT ------------------//
	
	@Override
	public RequestPairingResult requestDriveOpening(List<String> driveList,
			String pairingLabel) throws Y3Exception {
		assertConnected();
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("opening");
			if ( pairingLabel != null && !"".equals(pairingLabel)) {
				Element labelElt = doc.createElement("label");
				labelElt.setTextContent(pairingLabel);
				rootNode.appendChild(labelElt);
			}
			for ( String driveId:driveList ) {
				Element driveElt = doc.createElement("drive");
				driveElt.setTextContent(driveId);
				rootNode.appendChild(driveElt);
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		return sendHttpRequest(HttpMethod.POST, "/pairing/request", null,new RequestPairingResponseHandler(), content);
	}

	@Override
	public RequestPairingResult requestStrongAuthentication()
			throws Y3Exception {
		assertConnected();
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("auth");
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		return sendHttpRequest(HttpMethod.POST, "/pairing/request", null, new RequestPairingResponseHandler(), content);
	}
 
	@Override
	public RequestPairingResult requestAttach(String pairingLabel)
			throws Y3Exception {
		return requestAttach(pairingLabel,null);
	}


	@Override
	public RequestPairingResult requestBackupAttach(String pairingLabel)
			throws Y3Exception {
		return requestAttach(pairingLabel,null,true);
	}
	
	@Override
	public RequestPairingResult requestAttach(String pairingLabel,
			String rescuePEM) throws Y3Exception {
		return requestAttach(pairingLabel,rescuePEM,false);
	}
	
	public RequestPairingResult requestAttach(String pairingLabel,
			String rescuePEM, boolean forBackup) throws Y3Exception {
		assertConnected();
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("attach");
			if ( pairingLabel != null && !"".equals(pairingLabel)) {
				Element labelElt = doc.createElement("label");
				labelElt.setTextContent(pairingLabel);
				rootNode.appendChild(labelElt);
			}
			if ( rescuePEM != null && !"".equals(rescuePEM)) {
				Element pemElt = doc.createElement("pem");
				pemElt.setTextContent(rescuePEM);
				rootNode.appendChild(pemElt);
			}
			if ( forBackup ) {
				Element bkpElt = doc.createElement("backup");
				bkpElt.setTextContent("1");
				rootNode.appendChild(bkpElt);
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		return sendHttpRequest(HttpMethod.POST, "/pairing/request", null, new RequestPairingResponseHandler(), content);
	}

	@Override
	public FinalizePairingResult finalizePairing(String pairingId, String pairingPin)
			throws Y3Exception {
		assertConnected();
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("requestid", pairingId));
		qparams.add(new BasicNameValuePair("requestpin", pairingPin));
		return sendHttpRequest(HttpMethod.POST,"/pairing/finalize", qparams, new FinalizePairingResponseHandler(), null);
	}
	
	@Override
	public FinalizePairingResult finalizeAttachPairing(String pairingPin, String secretAESB64, String keyName, String controlCode)
			throws Y3Exception {
		assertConnected();
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("finalize");
			if ( pairingPin != null && !"".equals(pairingPin)) {
				Element labelElt = doc.createElement("code");
				labelElt.setTextContent(pairingPin);
				rootNode.appendChild(labelElt);
			}
			if ( secretAESB64 != null && !"".equals(secretAESB64)) {
				Element pemElt = doc.createElement("secret");
				pemElt.setTextContent(secretAESB64);
				rootNode.appendChild(pemElt);
			}
			if ( keyName != null && !"".equals(keyName)) {
				Element pemElt = doc.createElement("name");
				pemElt.setTextContent(keyName);
				rootNode.appendChild(pemElt);
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		return sendHttpRequest(HttpMethod.POST, "/pairing/finalizeattach", null,new FinalizePairingResponseHandler(), content);
	}
	
	@Override
	public GetPairingStatusResult getPairingStatus(String pairingId, String securityCode)
			throws Y3Exception {
		// assertConnected();
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("requestid", pairingId));
		if ( securityCode != null && !"".equals(securityCode))
			qparams.add(new BasicNameValuePair("seccode", securityCode));
		return sendHttpRequest(HttpMethod.POST,"/pairing/status", qparams, new GetPairingStatusResponseHandler(), null);
	}

	//----------------  ACCOUNT MANAGEMENT ------------------//
	@Override
	//TEMP Method to be removed after all users have migrated their key
	public void migratePEMKey(String PEM) throws Y3Exception {
		assertConnected();
		sendHttpRequest(HttpMethod.POST, "/account/migratepk", null,new EmptyReturnResponseHandler(), PEM);
	}
	

	@Override
	public void createUserAccount(String email, String firstName,
			String lastName, String passwordSha, String language) throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("create");
			XMLUtils.addChildText(doc, rootNode, "email", email);
			XMLUtils.addChildText(doc, rootNode, "firstname", firstName);
			XMLUtils.addChildText(doc, rootNode, "lastname", lastName);
			XMLUtils.addChildText(doc, rootNode, "passwordsha", passwordSha);
			XMLUtils.addChildText(doc, rootNode, "language", language);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/create", null,new EmptyReturnResponseHandler(), content);
	}
	
	@Override
	public void createUserAccount(String linkUniversalId, String email,
			String firstName, String lastName, String passwordSha,
			String language) throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("create");
			XMLUtils.addChildText(doc, rootNode, "email", email);
			XMLUtils.addChildText(doc, rootNode, "firstname", firstName);
			XMLUtils.addChildText(doc, rootNode, "lastname", lastName);
			XMLUtils.addChildText(doc, rootNode, "passwordsha", passwordSha);
			XMLUtils.addChildText(doc, rootNode, "language", language);
			XMLUtils.addChildText(doc, rootNode, "linkid", linkUniversalId);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/create", null,new EmptyReturnResponseHandler(), content);
		
	}

	@Override
	public void updateUserAccount(String firstName, String lastName,
			String passwordSha, String preferedLanguage,
			String company, String vatnumber, String address, String zip, String city, String country,
			String vatType, String rcsNumber) throws Y3Exception {
		
		//Prepare the content of the query as an XML body
		//Only put inside the request the params that are non null (null means no change)
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("update");
			if ( firstName != null )
				XMLUtils.addChildText(doc, rootNode, "firstname", firstName);
			if ( lastName != null)
				XMLUtils.addChildText(doc, rootNode, "lastname", lastName);
			if ( passwordSha != null)
				XMLUtils.addChildText(doc, rootNode, "passwordsha", passwordSha);
			if ( preferedLanguage != null)
				XMLUtils.addChildText(doc, rootNode, "language", preferedLanguage);
			if ( company != null)
				XMLUtils.addChildText(doc, rootNode, "company", company);
			if ( vatnumber != null)
				XMLUtils.addChildText(doc, rootNode, "vatnumber", vatnumber);
			if ( address != null)
				XMLUtils.addChildText(doc, rootNode, "address", address);
			if ( zip != null)
				XMLUtils.addChildText(doc, rootNode, "zip", zip);
			if ( city != null)
				XMLUtils.addChildText(doc, rootNode, "city", city);
			if ( country != null)
				XMLUtils.addChildText(doc, rootNode, "country", country);
			if ( vatType != null)
				XMLUtils.addChildText(doc, rootNode, "vattype", vatType);
			if ( rcsNumber != null)
				XMLUtils.addChildText(doc, rootNode, "rcsnumber", rcsNumber);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/update", null,new EmptyReturnResponseHandler(), content);
	}


	@Override
	public void validateUserAccount(String email, String validationCode)
			throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("validate");
			XMLUtils.addChildText(doc, rootNode, "email", email);
			XMLUtils.addChildText(doc, rootNode, "code", validationCode);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/validate", null,new EmptyReturnResponseHandler(), content);
	}


	@Override
	public void resetPassword(String email) throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("resetpassword");
			XMLUtils.addChildText(doc, rootNode, "email", email);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/resetpassword", null,new EmptyReturnResponseHandler(), content);
	}

	@Override
	public void resetPassword(String uniqueId, String newPasswordSha)
			throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("resetpasswordfinalize");
			XMLUtils.addChildText(doc, rootNode, "uid", uniqueId);
			XMLUtils.addChildText(doc, rootNode, "passwordsha", newPasswordSha);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/resetpasswordfinalize", null,new EmptyReturnResponseHandler(), content);
	}
	
	@Override
	public void unblockAccount(String email, String uniqueId)
			throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("unblock");
			XMLUtils.addChildText(doc, rootNode, "uid", uniqueId);
			XMLUtils.addChildText(doc, rootNode, "email", email);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/unblock", null,new EmptyReturnResponseHandler(), content);
	}

	@Override
	public void resentValidationEmail(String email, boolean externalAccount) throws Y3Exception {
		//Prepare the content of the query as an XML body
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("resentvalidationmail");
			XMLUtils.addChildText(doc, rootNode, "email", email);
			if ( externalAccount ) {
				XMLUtils.addChildText(doc, rootNode, "external", "1");
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST, "/account/resendvalidationemail", null,new EmptyReturnResponseHandler(), content);
	}
	
	@Override
	public GetAttachedKeysListResult getAttachedKeysList() throws Y3Exception {
		assertConnected();
		return sendHttpRequest(HttpMethod.POST,"/account/listpk", null, new GetAttachedKeysListResponseHandler(), null);
	}
	
	@Override
	public String paymentRequest() throws Y3Exception {
		assertConnected();
		return sendHttpRequest(HttpMethod.POST,"/account/payment", null, new StringReturnResponseHandler(), null);
	}

	@Override
	public void useVoucher(String voucherCode, boolean isTrial) throws Y3Exception {
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("voucher");
			rootNode.setTextContent(voucherCode);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		List<NameValuePair> qparams = null;
		if ( isTrial) {
			qparams = new ArrayList<NameValuePair>();
			qparams.add(new BasicNameValuePair("trial", "1"));
		}
		sendHttpRequest(HttpMethod.POST, "/account/voucher", qparams,new EmptyReturnResponseHandler(), content);
		
	}
	
	
	@Override
	public GetExternalAccountPairingResult getExternalAccountPairing(String uuid)
			throws Y3Exception {
		return sendHttpRequest(HttpMethod.POST,"/account/getexternal", null, new GetExternalAccountPairingResponseHandler(), uuid);
	}


	@Override
	public void setExternalAccountPairing(String uuid, String email,
			String firstName, String lastName, String language) throws Y3Exception {
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("update");
			if ( uuid != null && !"".equals(uuid)) {
				XMLUtils.addChildText(doc, rootNode, "uuid", uuid);
			}
			if ( email != null && !"".equals(email)) {
				XMLUtils.addChildText(doc, rootNode, "email", email);
			}
			if ( firstName != null && !"".equals(firstName)) {
				XMLUtils.addChildText(doc, rootNode, "firstname", firstName);
			}
			if ( lastName != null && !"".equals(lastName)) {
				XMLUtils.addChildText(doc, rootNode, "lastname", lastName);
			}
			if ( language != null && !"".equals(language))
				XMLUtils.addChildText(doc, rootNode, "language", language);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		sendHttpRequest(HttpMethod.POST,"/account/setexternal", null, new EmptyReturnResponseHandler(), content);
	}


	@Override
	public String completeExternalAccountPairing(String uuid,
			String validationCode) throws Y3Exception {
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("complete");
			if ( uuid != null && !"".equals(uuid)) {
				XMLUtils.addChildText(doc, rootNode, "uuid", uuid);
			}
			if ( validationCode != null && !"".equals(validationCode)) {
				XMLUtils.addChildText(doc, rootNode, "code", validationCode);
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		return sendHttpRequest(HttpMethod.POST,"/account/setexternal", null, new StringReturnResponseHandler(), content);
	}
	
	//----------------  DRIVE MANAGEMENT ------------------//
	
	
	
	@Override
	/**
	 * Call the getdrivelist REST method server-side
	 */
	public GetDriveListResult getDriveList() throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover the list of drives
		return sendHttpRequest(HttpMethod.GET, "/drive", null,new GetDriveListResponseHandler(), null);
	}
	
	@Override
	public GetDriveDetailsResult getDriveDetails(String driveId)
			throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover the list of drives
		return sendHttpRequest(HttpMethod.GET, "/drive/"+driveId, null,new GetDriveDetailsResponseHandler(), null);
	}

	
	
	//----------------  NODE MANAGEMENT ------------------//

	@Override
	public GetNodeContentResult getNodeContent(String driveId, String nodePath)
			throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		//Make a call to the rest API to recover content of requested node 
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId+nodePath,null, new GetNodeContentResponseHandler(), null);
	}
	
	@Override
	public GetNodeContentResult getNodeContent(String driveId, String nodePath,
			String nameFrom, int limit, String order) throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		if ( order == null || "".equals(order) ) {
			order = "ASC";
		}
		//Make a call to the rest API to recover content of requested node
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("from", nameFrom));
		qparams.add(new BasicNameValuePair("limit", ""+limit));
		qparams.add(new BasicNameValuePair("order", order));
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId+nodePath,qparams, new GetNodeContentResponseHandler(), null);
	}
	
	@Override
	public GetNodeDetailsResult getNodeDetails(String driveId, String nodePath)
			throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		//Make a call to the rest API to recover content of requested node
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("details", "1"));
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId+nodePath,qparams, new GetNodeDetailsResponseHandler(), null);
	}


	@Override
	public GetNodeDetailsResult getNodeDetailsByUid(String driveId, String nodeUid)
			throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover content of requested node 
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		qparams.add(new BasicNameValuePair("details", "1"));
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId,qparams, new GetNodeDetailsResponseHandler(), null);
	}
	
	@Override
	public void createNode(String drive, String nodePath) throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		sendHttpRequest(HttpMethod.PUT, "/node/"+drive+nodePath,null, new EmptyReturnResponseHandler(), null);		
	}


	@Override
	public GetNodeContentResult getNodeContentByUid(String driveId,
			String nodeUid) throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover content of requested node 
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId,qparams, new GetNodeContentResponseHandler(), null);
	}

	@Override
	public GetNodeContentResult getNodeContentByUid(String driveId,
			String nodeUid, String nameFrom, int limit, String order) throws Y3Exception {
		assertConnected();
		if ( order == null || "".equals(order) ) {
			order = "ASC";
		}
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		qparams.add(new BasicNameValuePair("from", nameFrom));
		qparams.add(new BasicNameValuePair("limit", ""+limit));
		qparams.add(new BasicNameValuePair("order", order));
		return sendHttpRequest(HttpMethod.GET, "/node/"+driveId,qparams, new GetNodeContentResponseHandler(), null);
	}
	
	@Override
	public void renameNode(String driveId, String nodePath, String newName)
			throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		
		String request = renameRequestContent(newName);
		if ( request == null ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		
		sendHttpRequest(HttpMethod.POST,"/node/"+driveId+nodePath, null, new EmptyReturnResponseHandler(), request);
	}


	@Override
	public void renameNodeByUId(String driveId, String nodeUid, String newName)
			throws Y3Exception {
		assertConnected();
		String request = renameRequestContent(newName);
		if ( request == null ) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		sendHttpRequest(HttpMethod.POST,"/node/"+driveId, qparams, new EmptyReturnResponseHandler(), request);
	}
	
	private String renameRequestContent(String newName) {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("rename");
			rootNode.setTextContent(newName);
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			return null;
		}
	}
	

	@Override
	public void finalizeQuickSharing(String driveId, String sharingPath,
			String timestamp, String recipients, String message) throws Y3Exception {
		Document doc = null;
		String content = null;
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			Element rootNode = doc.createElement("finalizequickshare");
			XMLUtils.addChildText(doc, rootNode, "timestamp", timestamp);
			XMLUtils.addChildText(doc, rootNode, "message", message);
			XMLUtils.addChildText(doc, rootNode, "recipients", recipients);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_INTERNAL, "Error creating request content",ex);
		}
		
		if ( !"".equals(sharingPath) && sharingPath.indexOf("/") != 0 ) {
			sharingPath = "/" + sharingPath;
		}
		sendHttpRequest(HttpMethod.POST, "/node/"+driveId+sharingPath, null,new EmptyReturnResponseHandler(), content);
	}

	
	private String addRemoveRequestContent(String method, String type, String[] members, String message) throws Y3Exception {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement(method);
			if ( type != null && !"".equals(type))
				XMLUtils.addChildText(doc, rootNode, "type", type);
			if ( message != null && !"".equals(message))
				XMLUtils.addChildText(doc, rootNode, "message", message);
			Element membersElt = doc.createElement("members");
			if ( members != null && members.length > 0 ) {
				for (String aMember : members) {
					XMLUtils.addChildText(doc, membersElt, "member", aMember);
				}
			}
			rootNode.appendChild(membersElt);
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
	}
	
	private ManageMemberResult addRemoveMembers(String type, boolean remove, String driveId, String nodePath, String[] members, String message) throws Y3Exception {
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		String request = addRemoveRequestContent(remove?"removemembers":"addmembers", type, members, message);
		return sendHttpRequest(HttpMethod.POST,"/node/"+driveId+nodePath, null, new ManageMemberResponseHandler(), request);
	}
	
	private ManageMemberResult addRemoveMembersByUid(String type, boolean remove, String driveId, String nodeUid, String[] members, String message) throws Y3Exception {
		String request = addRemoveRequestContent(remove?"removemembers":"addmembers", type, members, message);
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		return sendHttpRequest(HttpMethod.POST,"/node/"+driveId, qparams, new ManageMemberResponseHandler(), request);
	}
	
	@Override
	public ManageMemberResult addReaders(String driveId, String nodePath, String[] newReaders, String message) throws Y3Exception {
		assertConnected();
		return addRemoveMembers("readers", false, driveId, nodePath, newReaders, message);
	}

	@Override
	public ManageMemberResult addWriters(String driveId, String nodePath, String[] newWriters, String message) throws Y3Exception {
		assertConnected();
		return addRemoveMembers("writers", false, driveId, nodePath, newWriters, message);		
	}

	@Override
	public ManageMemberResult addReadersByUId(String driveId, String nodeUid, String[] newReaders, String message) throws Y3Exception {
		assertConnected();
		return addRemoveMembersByUid("readers", false, driveId, nodeUid, newReaders, message);
	}


	@Override
	public ManageMemberResult addWritersByUId(String driveId, String nodeUid, String[] newWriters, String message) throws Y3Exception {
		assertConnected();
		return addRemoveMembersByUid("writers", false, driveId, nodeUid, newWriters, message);
	}

	@Override
	public ManageMemberResult removeReaders(String driveId, String nodePath, String[] readersToRemove) throws Y3Exception {
		assertConnected();
		return addRemoveMembers("readers", true, driveId, nodePath, readersToRemove, null);
	}

	@Override
	public ManageMemberResult removeWriters(String driveId, String nodePath, String[] writersToRemove) throws Y3Exception {
		assertConnected();
		return addRemoveMembers("writers", true, driveId, nodePath, writersToRemove, null);
	}

	@Override
	public ManageMemberResult removeReadersByUId(String driveId, String nodeUid, String[] readersToRemove) throws Y3Exception {
		assertConnected();
		return addRemoveMembersByUid("readers", true, driveId, nodeUid, readersToRemove, null);
	}


	@Override
	public ManageMemberResult removeWritersByUId(String driveId, String nodeUid, String[] writersToRemove) throws Y3Exception {
		assertConnected();
		return addRemoveMembersByUid("writers", true, driveId, nodeUid, writersToRemove, null);
	}


	@Override
	public DeleteNodeResult deleteNode(String driveId, String nodePath) throws Y3Exception {
		assertConnected();
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		return sendHttpRequest(HttpMethod.DELETE,"/node/"+driveId+nodePath, null, new DeleteNodeResponseHandler(), null);		
	}

	

	@Override
	public ManageMemberResult addOwners(String driveId, String[] newOwners)
			throws Y3Exception {
		String request = addRemoveRequestContent("addowners", null, newOwners, null);
		return sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new ManageMemberResponseHandler(), request);
	}


	@Override
	public ManageMemberResult removeOwners(String driveId,
			String[] ownersToRemove) throws Y3Exception {
		String request = addRemoveRequestContent("removeowners", null, ownersToRemove, null);
		return sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new ManageMemberResponseHandler(), request);
	}


	@Override
	public ManageMemberResult programUserKey(String driveId,
			String[] usersToProgram) throws Y3Exception {
		assertConnected();
		String request = addRemoveRequestContent("programkeys", null, usersToProgram, null);
		return sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new ManageMemberResponseHandler(), request);
	}


	@Override
	public ManageMemberResult revokeUserKey(String driveId,
			String[] usersToRevoke) throws Y3Exception {
		assertConnected();
		String request = addRemoveRequestContent("revokekeys", null, usersToRevoke, null);
		return sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new ManageMemberResponseHandler(), request);
	}
	
	@Override
	public void activateSafe(String driveId) throws Y3Exception {
		assertConnected();
		String request = activateSafeRequestContent();
		sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new EmptyReturnResponseHandler(), request);
	}	
	
	private String activateSafeRequestContent() throws Y3Exception {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("activatesafe");
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
	}

	@Override
	public void createDrive(String driveId, String friendlyName, String language, String mailPolicy, String publishPolicy, boolean highConfientiality)
			throws Y3Exception {
		assertConnected();
		String request = createUpdateDriveRequestContent(false, driveId, friendlyName, language, mailPolicy, highConfientiality, publishPolicy, null);
		sendHttpRequest(HttpMethod.POST,"/drive",null, new EmptyReturnResponseHandler(), request);
	}
	
	@Override
	public void updateDrive(String driveId, String friendlyName,
			String preferedLanguage, String mailPolicy, String publishPolicy, String downloadPolicy) throws Y3Exception {
		assertConnected();
		String request = createUpdateDriveRequestContent(true, driveId, friendlyName, preferedLanguage, mailPolicy, false, publishPolicy, downloadPolicy);
		sendHttpRequest(HttpMethod.POST,"/drive/"+driveId,null, new EmptyReturnResponseHandler(), request);
	}
	
	@Override
	public void deleteDrive(String driveId) throws Y3Exception {
		assertConnected();
		sendHttpRequest(HttpMethod.DELETE,"/drive/"+driveId,null, new EmptyReturnResponseHandler(), null);
	}
	
	@Override
	public String getPathFromUid(String driveId, String uid) throws Y3Exception {
		assertConnected();
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", uid));
		return sendHttpRequest(HttpMethod.GET,"/drive/"+driveId,qparams, new StringReturnResponseHandler(), null);
	}
	
	private String createUpdateDriveRequestContent(boolean update, String id, String name, String language, String mailPolicy, boolean highConfientiality, String publishPolicy, String dlPolicy) throws Y3Exception {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode; 
			if ( update)
				rootNode = doc.createElement("updatedrive");
			else
				rootNode = doc.createElement("createdrive");
			if ( !update)
				XMLUtils.addChildText(doc, rootNode, "id", id);
			
			if ( name != null && !"".equals(name))
				XMLUtils.addChildText(doc, rootNode, "name", name);
			if ( language != null && !"".equals(language))
				XMLUtils.addChildText(doc, rootNode, "language", language);
			if ( mailPolicy != null && !"".equals(mailPolicy))
				XMLUtils.addChildText(doc, rootNode, "mailpolicy", mailPolicy);
			if ( publishPolicy != null && !"".equals(publishPolicy))
				XMLUtils.addChildText(doc, rootNode, "publishpolicy", publishPolicy);
			if ( dlPolicy != null && !"".equals(dlPolicy))
				XMLUtils.addChildText(doc, rootNode, "downloadpolicy", dlPolicy);
			if ( highConfientiality )
				XMLUtils.addChildText(doc, rootNode, "esafe", "1");
			
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
	}
	

	@Override
	public DeleteNodeResult deleteNodeByUid(String driveId, String nodeUid)
			throws Y3Exception {
		assertConnected();
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		return sendHttpRequest(HttpMethod.DELETE, "/node/"+driveId,qparams, new DeleteNodeResponseHandler(), null);		
	}
	
	
	//----------------  LEAF MANAGEMENT ------------------//
	private String publishLeafContent(String message) throws Y3Exception {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("publish");
			if ( message != null && !"".equals(message))
				XMLUtils.addChildText(doc, rootNode, "message", message);
			
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
	}
	
	@Override
	public void publishLeaf(String driveId, String nodePath, String message)
			throws Y3Exception {
		assertConnected();
		String request = publishLeafContent(message);
		if ( !"".equals(nodePath) && nodePath.indexOf("/") != 0 ) {
			nodePath = "/" + nodePath;
		}
		sendHttpRequest(HttpMethod.POST,"/leaf/"+driveId+nodePath, null, new EmptyReturnResponseHandler(), request);
	}


	@Override
	public void publishLeafByUid(String driveId, String nodeUid, String message)
			throws Y3Exception {
		assertConnected();
		String request = publishLeafContent(message);
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", nodeUid));
		sendHttpRequest(HttpMethod.POST,"/leaf/"+driveId, qparams, new EmptyReturnResponseHandler(), request);
	}
	
	@Override
	public LeafContentMetadata getLeafContent(OutputStream destination, String driveId, String leafPath)
			throws Y3Exception {
		return getLeafContent(destination, driveId, leafPath, null);
	}

	@Override
	public LeafContentMetadata getLeafContent(OutputStream destination,
			String driveId, String leafPath, String version) throws Y3Exception {
		assertConnected();
		if ( !"".equals(leafPath) && leafPath.indexOf("/") != 0 ) {
			leafPath = "/" + leafPath;
		}
		
		List<NameValuePair> qparams = null; 
		if ( version !=null && !"".equals(version) ) { 
			qparams = new ArrayList<NameValuePair>();
			qparams.add(new BasicNameValuePair("version", version));
		}
		
		//Make a call to the rest API to recover content of requested node 
		return sendHttpRequest(HttpMethod.GET, "/leaf/"+driveId+leafPath,qparams, new GetLeafContentMetadataAndStreamResponseHandler(destination), null);
	}

	@Override
	public LeafContentMetadata getLeafContentByUid(OutputStream destination, String driveId,
			String leafUid) throws Y3Exception {
		return getLeafContentByUid(destination,driveId,leafUid,null);
	}
	
	@Override
	public LeafContentMetadata getLeafContentByUid(OutputStream destination,
			String driveId, String leafUid, String version) throws Y3Exception {
		assertConnected();
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", leafUid));
		if ( version !=null && !"".equals(version) ) {
			qparams.add(new BasicNameValuePair("version", version));
		}
		
		//Make a call to the rest API to recover content of requested node 
		return sendHttpRequest(HttpMethod.GET, "/leaf/"+driveId, qparams, new GetLeafContentMetadataAndStreamResponseHandler(destination), null);
	}

	@Override
	public String getDownloadLinkForUid(String driveId, String leafUid,
			String version) throws Y3Exception {
		assertConnected();
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", leafUid));
		if ( version !=null && !"".equals(version) ) {
			qparams.add(new BasicNameValuePair("version", version));
		}
		//Make a call to the rest API to recover content of requested node 
		return sendHttpRequest(HttpMethod.POST, "/link/"+driveId, qparams, new StringReturnResponseHandler(), null);
	}

	@Override
	public LeafContentMetadata getLeafContentMetadata(String driveId,
			String leafPath) throws Y3Exception {
		return getLeafContentMetadata(driveId,leafPath,null);
	}
	
	@Override
	public LeafContentMetadata getLeafContentMetadata(String driveId,
			String leafPath, String version) throws Y3Exception {
		assertConnected();
		if ( !"".equals(leafPath) && leafPath.indexOf("/") != 0 ) {
			leafPath = "/" + leafPath;
		}
		
		List<NameValuePair> qparams = null; 
		
		if ( version !=null && !"".equals(version) ) { 
			qparams = new ArrayList<NameValuePair>();
			qparams.add(new BasicNameValuePair("version", version));
		}
		
		//Make a call to the rest API to recover metadata of requested node 
		return sendHttpRequest(HttpMethod.HEAD, "/leaf/"+driveId+leafPath,qparams, new GetLeafContentMetadataAndStreamResponseHandler(null), null);
	}

	@Override
	public LeafContentMetadata getLeafContentMetadataByUid(String driveId,
			String leafUid) throws Y3Exception {
		return getLeafContentMetadataByUid(driveId,leafUid,null);
	}
	
	@Override
	public LeafContentMetadata getLeafContentMetadataByUid(String driveId,
			String leafUid, String version) throws Y3Exception {
		assertConnected();
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", leafUid));
		
		if ( version !=null && !"".equals(version) )
			qparams.add(new BasicNameValuePair("version", version));
		//Make a call to the rest API to recover content of requested node 
		return sendHttpRequest(HttpMethod.GET, "/leaf/"+driveId, qparams, new GetLeafContentMetadataAndStreamResponseHandler(null), null);
	}

	
	@Override
	public void assertCanSetLeafContent(String driveId, String leafPath, long size)
			throws Y3Exception {
		assertConnected();
		if ( !"".equals(leafPath) && leafPath.indexOf("/") != 0 ) {
			leafPath = "/" + leafPath;
		}
		if ( size < 0 ) {
			size = -1;
		}
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("size", ""+size));
		qparams.add(new BasicNameValuePair("check", "1"));
		
		//Make a call to the rest API to check if we can access this path 
		sendHttpRequest(HttpMethod.PUT, "/leaf/"+driveId+leafPath,qparams, new EmptyReturnResponseHandler(), null);
	}


	@Override
	public void assertCanSetLeafContentByUid(String driveId, String leafUid, long size)
			throws Y3Exception {
		assertConnected();
		if ( size < 0 ) {
			size = -1;
		}
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", leafUid));
		qparams.add(new BasicNameValuePair("size", ""+size));
		qparams.add(new BasicNameValuePair("check", "1"));
		
		//Make a call to the rest API to recover content of requested node 
		sendHttpRequest(HttpMethod.PUT, "/leaf/"+driveId,qparams, new EmptyReturnResponseHandler(), null);
	}
	
	@Override
	public void setLeafContent(InputStream source, LeafContentMetadata metadata, String driveId, String leafPath, String action, String actionParam) throws Y3Exception {
		assertConnected();
		if ( !"".equals(leafPath) && leafPath.indexOf("/") != 0 ) {
			leafPath = "/" + leafPath;
		}
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		if ( action != null && !"".equals(action)) {
			qparams.add(new BasicNameValuePair("action", action));
			if ( actionParam != null && !"".equals(actionParam)) {
				qparams.add(new BasicNameValuePair("actionparam", actionParam));
			}
		}
		//Make a call to the rest API to recover content of requested node 
		sendHttpRequest(HttpMethod.PUT, "/leaf/"+driveId+leafPath,qparams, new EmptyReturnResponseHandler(), null, source, metadata);
	}

	@Override
	public void setLeafContentByUid(InputStream source,
			LeafContentMetadata metadata, String driveId, String leafUid, String action, String actionParam)
			throws Y3Exception {
		assertConnected();
		
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", leafUid));
		if ( action != null && !"".equals(action)) {
			qparams.add(new BasicNameValuePair("action", action));
			if ( actionParam != null && !"".equals(actionParam)) {
				qparams.add(new BasicNameValuePair("actionparam", actionParam));
			}
		}
		//Make a call to the rest API to recover content of requested node 
		sendHttpRequest(HttpMethod.PUT, "/leaf/"+driveId, qparams, new EmptyReturnResponseHandler(), null, source, metadata);
	}
	
	@Override
	public void setOriginatingAddr(String addr) {
		this.originatingAddr = addr;
	}
	
	@Override
	public void setUserUUID(String uuid) {
		this.userUUID = uuid;
	}
	

	@Override
	public void setBrandingLogo(InputStream stream,
			LeafContentMetadata metadata, String type) throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover content of requested node
		sendHttpRequest(HttpMethod.PUT, "/account/brandinglogo"+type,null, new EmptyReturnResponseHandler(), null, stream, metadata);
	}
	
	public void delBrandingLogo(String type) throws Y3Exception {
		assertConnected();
		//Make a call to the rest API to recover content of requested node
		sendHttpRequest(HttpMethod.DELETE, "/account/brandinglogo"+type,null, new EmptyReturnResponseHandler(), null);
	}


	@Override
	public void setBrandingColor(KeyValue[] values) throws Y3Exception {
		assertConnected();
		String content = null;
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("customcss");
			for ( KeyValue kv : values ) {
				Element entry = doc.createElement("entry");
				XMLUtils.addChildText(doc, entry, "key"  , kv.key);
				XMLUtils.addChildText(doc, entry, "value", kv.value);
				rootNode.appendChild(entry);
			}
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		sendHttpRequest(HttpMethod.POST, "/account/brandingcss",null, new EmptyReturnResponseHandler(), content);
	}


	@Override
	public GetBrandingResult getBranding(String drive) throws Y3Exception {
		assertConnected();
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("branding", "1"));
		drive = drive == null ? "" : drive;
		return sendHttpRequest(HttpMethod.GET,"/drive/"+drive,qparams, new GetBrandingResponseHandler(), null);
	}
	
	@Override
	public GetItemLinkDetailsResult getItemLinkDetails(String linkId,
			String email) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", linkId));
		qparams.add(new BasicNameValuePair("email", email));
		return sendHttpRequest(HttpMethod.GET,"/link",qparams, new GetItemLinkDetailsResponseHandler(), null);
	}

	@Override
	public LeafContentMetadata getItemLinkContent(OutputStream destination,
			String linkId, String email) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("uid", linkId));
		qparams.add(new BasicNameValuePair("email", email));
		qparams.add(new BasicNameValuePair("content", "1"));		
		return sendHttpRequest(HttpMethod.GET,"/link",qparams, new GetLeafContentMetadataAndStreamResponseHandler(destination), null);
	}
	

	@Override
	public GetUserListResult getUserList() throws Y3Exception {
		return sendHttpRequest(HttpMethod.GET,"/admin/users",null, new GetUserListResponseHandler(), null);
	}
	
	@Override
	public GetUserListResult getUserList(String emailFrom, int limit) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("from", emailFrom));
		qparams.add(new BasicNameValuePair("limit", ""+limit));
		return sendHttpRequest(HttpMethod.GET,"/admin/users",qparams, new GetUserListResponseHandler(), null);
	}


	private String createAddUpdateUserRequestContent(String email, String firstName, String lastName, String role, String pass, boolean update) throws Y3Exception {
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode; 
			if ( update)
				rootNode = doc.createElement("updateuser");
			else
				rootNode = doc.createElement("createuser");
			
			
			XMLUtils.addChildText(doc, rootNode, "email", email);
			if ( firstName != null && !"".equals(firstName))
				XMLUtils.addChildText(doc, rootNode, "firstname", firstName);
			if ( lastName != null && !"".equals(lastName))
				XMLUtils.addChildText(doc, rootNode, "lastname", lastName);
			if ( role != null && !"".equals(role))
				XMLUtils.addChildText(doc, rootNode, "role", role);
			if ( pass != null && !"".equals(pass))
				XMLUtils.addChildText(doc, rootNode, "passwordsha", pass);
			return XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
	}
	
	@Override
	public void addUser(String email, String firstName, String lastName, String passsha,
			String role) throws Y3Exception {
		String content = createAddUpdateUserRequestContent(email, firstName, lastName, role, passsha, false);
		sendHttpRequest(HttpMethod.POST,"/admin/users",null, new EmptyReturnResponseHandler(), content);		
	}


	@Override
	public void updUser(String email, String firstName, String lastName,
			String role) throws Y3Exception {
		String content = createAddUpdateUserRequestContent(email, firstName, lastName, role, null, true);
		sendHttpRequest(HttpMethod.POST,"/admin/users",null, new EmptyReturnResponseHandler(), content);
	}


	@Override
	public void delUser(String email) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("email", email));
		sendHttpRequest(HttpMethod.DELETE,"/admin/users",qparams, new EmptyReturnResponseHandler(), null);
	}
	
	@Override
	public GetEditionResult getEdition() throws Y3Exception {
		return sendHttpRequest(HttpMethod.GET, "/version", null, new GetEditionResponseHandler(), null);
	}
	
	
	@Override
	public CreateFlowResult createFlow(String baseName, String activationCode) throws Y3Exception {
		String content = null;
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("createflow");
			if ( baseName != null && !"".equals(baseName))
				XMLUtils.addChildText(doc, rootNode, "basename", baseName);
			if ( activationCode != null && !"".equals(activationCode))
				XMLUtils.addChildText(doc, rootNode, "activationcode", activationCode);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		return sendHttpRequest(HttpMethod.POST,"/flows/create",null, new CreateFlowResponseHandler(), content);
	}


	@Override
	public CreateFlowEventResult createFlowEvent(String flowAlias, String flowKey, String name,
			String message) throws Y3Exception {
		String content = null;
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("createflowevent");
			XMLUtils.addChildText(doc, rootNode, "alias", flowAlias);
			if ( flowKey != null && !"".equals(flowKey))
				XMLUtils.addChildText(doc, rootNode, "key", flowKey);
			if ( name != null && !"".equals(name))
				XMLUtils.addChildText(doc, rootNode, "name", name);
			if ( message != null && !"".equals(message))
				XMLUtils.addChildText(doc, rootNode, "message", message);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		return sendHttpRequest(HttpMethod.POST,"/flows/createevent",null, new CreateFlowEventResponseHandler(), content);
	}
	

	@Override
	public void finalizeFlowEvent(String flowAlias, String flowKey,
			String eventCid) throws Y3Exception {
		String content = null;
		try {
			Document doc = null;
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element rootNode = doc.createElement("finalizeflowevent");
			XMLUtils.addChildText(doc, rootNode, "alias", flowAlias);
			if ( flowKey != null && !"".equals(flowKey))
				XMLUtils.addChildText(doc, rootNode, "key", flowKey);
			if ( eventCid != null && !"".equals(eventCid))
				XMLUtils.addChildText(doc, rootNode, "cid", eventCid);
			content = XMLUtils.elementToString(rootNode);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Unable to prepare request content");
		}
		sendHttpRequest(HttpMethod.POST,"/flows/finalizeevent",null, new EmptyReturnResponseHandler(), content);
	}


	@Override
	public GetFlowResult getFlow(String flowAlias, String flowKey) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		if ( flowKey != null && !"".equals(flowKey))
			qparams.add(new BasicNameValuePair("key", flowKey));
		return sendHttpRequest(HttpMethod.GET,"/flows/"+flowAlias,qparams, new GetFlowResponseHandler(), null);
	}


	@Override
	public GetFlowEventResult getFlowEvent(String flowAlias, String flowKey, String eventCid)
			throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("eventid", eventCid));
		if ( flowKey != null && !"".equals(flowKey))
			qparams.add(new BasicNameValuePair("key", flowKey));
		return sendHttpRequest(HttpMethod.GET,"/flows/"+flowAlias, qparams, new GetFlowEventResponseHandler(), null);
	}
	
	@Override
	public void deleteFlowEvent(String flowAlias, String flowKey,
			String eventCid) throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("eventid", eventCid));
		if ( flowKey != null && !"".equals(flowKey))
			qparams.add(new BasicNameValuePair("key", flowKey));
		sendHttpRequest(HttpMethod.DELETE,"/flows/"+flowAlias, qparams, new EmptyReturnResponseHandler(), null);
	}
	
	@Override
	public GetFlowUpdateResult getFlowUpdates(String flowAlias, String flowKey, String systemTs)
			throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		qparams.add(new BasicNameValuePair("update", systemTs));
		if ( flowKey != null && !"".equals(flowKey))
			qparams.add(new BasicNameValuePair("key", flowKey));
		return sendHttpRequest(HttpMethod.GET,"/flows/"+flowAlias, qparams, new GetFlowUpdateResponseHandler(), null);
	}
	
	@Override
	public GetFlowsListResult getFlowsList(String nameFrom, int limit)
			throws Y3Exception {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>();
		if ( nameFrom != null && !"".equals(nameFrom))
			qparams.add(new BasicNameValuePair("from", nameFrom));
		if ( limit > 0)
			qparams.add(new BasicNameValuePair("limit", ""+limit));
		return sendHttpRequest(HttpMethod.GET,"/flows-admin", qparams, new GetFlowsListResponseHandler(), null);
	}

	@Override
	public String playAdminCommand(String command) throws Y3Exception {
		return sendHttpRequest(HttpMethod.POST,"/flows-admin", null, new StringReturnResponseHandler(), command);
	}
	
	
	@Override
	public GetFlowsConfigurationResult getFlowsConfiguration()
			throws Y3Exception {
		return sendHttpRequest(HttpMethod.GET,"/flows/", null, new GetFlowsConfigurationResponseHandler(), null);
	}


	
	private void assertConnected() throws Y3Exception {
		//Check if connected otherwise throw exception
		if ( token == null || "".equals(token)) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_NOACCESS,"No token attached to the client, use login() or attachToken() prior to any server calls");
		}
	}
	
	/**
	 * Call the requested targetURI with the requested method.
	 * Adds every required information into the request header.
	 * 
	 * @param method
	 * @param targetURI
	 * @throws Y3Exception
	 */
	private <T> T sendHttpRequest(HttpMethod method, String targetPath, List<NameValuePair> qparams, ResponseHandler<T> handler, String rawContent) throws Y3Exception {
		return sendHttpRequest(method, targetPath, qparams, handler, rawContent, null, null);
	}
	private <T> T sendHttpRequest(HttpMethod method, String targetPath, List<NameValuePair> qparams, ResponseHandler<T> handler, String rawContent, InputStream inputStream) throws Y3Exception {
		return sendHttpRequest(method, targetPath, qparams, handler, rawContent, inputStream, new LeafContentMetadata());
	}
	private <T> T sendHttpRequest(HttpMethod method, String targetPath, List<NameValuePair> qparams, ResponseHandler<T> handler, String rawContent, InputStream inputStream, LeafContentMetadata metadata) throws Y3Exception {
		//Create client
		HttpClient httpClient = HttpClientFactory.buildHttpClient();

		//Compute destination using URI to escape properly parts of the URI
		String path = basePath + targetPath;
		//Escape all spaces in the path
		path = path.replaceAll(" ", "%20");
		
		URI uri = null;
		try {
			//uri = new URI(scheme,host, path, queryString, null);
			//uri = URIUtils.createURI(scheme, host, -1, path, queryString, null);
			//http://stackoverflow.com/questions/883136/is-there-a-good-url-builder-for-java
			uri = URIUtils.createURI(scheme, host, -1, path, qparams == null ? null : URLEncodedUtils.format(qparams, "UTF-8"), null);
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP,"Invalid URI", ex);
		}

		String destination = uri.toASCIIString();
		
		//Build request
		HttpUriRequest httpRequest = null;
		if ( method == HttpMethod.POST) {
			httpRequest = new HttpPost(destination);
			if ( rawContent != null ) {
				try {
					StringEntity entity = new StringEntity(rawContent,"UTF-8"); 
					((HttpPost)httpRequest).setEntity(entity);
				} catch (Exception ex) {
					throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP, "Unable to add request content",ex);
				}
			}
		} else if ( method == HttpMethod.GET) {
			httpRequest = new HttpGet(destination);
		} else if ( method == HttpMethod.HEAD) {
			httpRequest = new HttpHead(destination);
		} else if ( method == HttpMethod.PUT) {
			httpRequest = new HttpPut(destination);
			httpRequest .getParams().setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, true);
			if ( inputStream != null ) {
				try {
					InputStreamEntity entity = new InputStreamEntity(inputStream, metadata.contentLength);
					((HttpPut)httpRequest).setEntity(entity);
				} catch (Exception ex) {
					throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP, "Unable to add request input stream",ex);	
				}
			}
		} else if ( method == HttpMethod.DELETE) {
			httpRequest = new HttpDelete(destination);
		}
				
		//Add headers
		if ( metadata != null ) {
			httpRequest.addHeader("Content-Type", (metadata.contentType==null||"".equals(metadata.contentType))?"application/octet-stream":metadata.contentType);
			if ( metadata.contentMD5 != null && !"".equals(metadata.contentMD5)) {
				httpRequest.addHeader("Content-MD5", metadata.contentMD5);
			}
		}
		
		if ( userName != null ) {
			httpRequest.addHeader("Authorization",userName);
		}
		
		//If we know a token, add it to the request
		if ( token != null && !"".equals(token)) {
			httpRequest.addHeader("y3-token-id",token);
		}
		
		//If we have a secret add it to the request
		if ( safeSecret != null && !"".equals(safeSecret)) {
			httpRequest.addHeader("y3-user-secret",safeSecret);
		}

		//Add originating addr if known
		if ( originatingAddr != null && !"".equals(originatingAddr) ) {
			httpRequest.addHeader("y3-originating-addr",originatingAddr);
		}
		
		//Add UUID if present
		if ( userUUID != null && !"".equals(userUUID)) {
			httpRequest.addHeader("y3-user-uuid",userUUID);
		}
		
		//Add keyserial if present
		if ( keySerial != null && !"".equals(keySerial)) {
			httpRequest.addHeader("y3-ekey-serial",keySerial);
		}
		
		/*
		 * If an API Key is provided compute the signature
		 * Signature is for the moment done only on the date of the request, date must be compliant to the RFC822 format (like for AWS)
		 */
		if ( apiKey != null && apiSecret != null ) {
			
			String formatedDate = rfc822DateFormat.format(new Date());
			String toSign = formatedDate;
			String signature = CryptoUtils.getB64HMAC(apiSecret, toSign);
			httpRequest.addHeader("Date", formatedDate);
			httpRequest.addHeader("y3-api-key",apiKey);
			httpRequest.addHeader("y3-api-sign",signature);
		}
		
		//Call service
		T response = null;
		try {
			long startTime = System.currentTimeMillis();
			if ( logger.isDebugEnabled() ) {
				logger.debug("Calling "+method+" "+destination);
			}
			response = httpClient.execute(httpRequest,handler);
			if ( logger.isTraceEnabled() ) {
				logger.trace("[PERF] "+method+" "+destination+ " -> "+(System.currentTimeMillis()-startTime)+" ms");
			}
		} catch (ClientProtocolException pex) {
			if ( pex.getCause() != null && pex.getCause() instanceof Y3Exception ) {
				throw (Y3Exception) pex.getCause();
			} else {
				//Error in the httpClient object -> http error
				throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP, "Error during HTTPClient.execute()",pex);
			}
		} catch (Exception ex) {
			throw new Y3Exception(Y3ErrorCode.ERRCODE_HTTP, "Error during HTTPClient.execute()",ex);
		}

		return response;
	}


}
